import { encodings } from './encodings/index.js';
import { PrependBOMEncoderWrapper, StripBOMDecoderWrapper } from './bom-handling.js';
import { Codec, CodecOption, Decoder, Encoder, EncodingOption, Options } from './models.js';

class Iconv {
  private codecDataCache: { [key: string]: Codec } = {};

  /**
   * 使用指定字符集解码
   *
   * @param {Buffer | Uint8Array} buffer
   * @param {string} encoding
   * @param {Options} options
   * @return {string}
   */
  public decode(buffer: Buffer | Uint8Array, encoding: string, options?: Options): string {
    const decoder = this.getDecoder(encoding, options);

    const buf = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);

    const res = decoder.write(buf);
    const trail = decoder.end() ?? '';
    return res + trail;
  }

  /**
   * 使用指定字符集编码
   *
   * @param {string} content
   * @param {string} encoding
   * @param {Options} options
   * @return {Buffer}
   */
  public encode(content: string, encoding: string, options?: Options): Buffer {
    const encoder = this.getEncoder(encoding, options);

    const res = encoder.write(content);
    const trail = encoder.end();
    return trail && trail.length > 0 ? Buffer.concat([res, trail]) : res;
  }

  /**
   * 判断字符集是否存在
   *
   * @param {string} encoding
   * @return {boolean}
   */
  public encodingExists(encoding: string): boolean {
    return Object.prototype.hasOwnProperty.call(encodings, this.canonicalizeEncoding(encoding));
  }

  /**
   * 获取指定字符集解码器
   *
   * @param {string} encoding
   * @param {Options} options
   * @return {Decoder}
   */
  public getDecoder(encoding: string, options?: Options): Decoder {
    const codec = this.getCodec(encoding);
    let decoder = new codec.decoder(options, codec);

    if (codec.bomAware && !(options && options.stripBOM === false)) {
      decoder = new StripBOMDecoderWrapper(decoder, options);
    }

    return decoder;
  }

  /**
   * 获取指定字符集编码器
   *
   * @param {string} encoding
   * @param {Options} options
   * @return {Encoder}
   */
  public getEncoder(encoding: string, options?: Options): Encoder {
    const codec = this.getCodec(encoding);
    let encoder = new codec.encoder(options, codec);

    if (codec.bomAware && options && options.addBOM) {
      encoder = new PrependBOMEncoderWrapper(encoder, options);
    }

    return encoder;
  }

  private getCodec(encoding: string): Codec {
    let enc = this.canonicalizeEncoding(encoding);
    let codecOptions: Partial<CodecOption> = {};

    let codec = this.codecDataCache[enc];
    while (codec === undefined) {
      const codecDef = encodings[enc];

      switch (typeof codecDef) {
        case 'string':
          enc = codecDef;
          break;

        case 'object':
          codecOptions = { ...codecOptions, ...(codecDef as EncodingOption) };

          if (!codecOptions.encodingName) codecOptions.encodingName = enc;

          enc = codecDef.type;
          break;

        case 'function':
          if (!codecOptions.encodingName) codecOptions.encodingName = enc;

          codec = new codecDef(codecOptions);
          this.codecDataCache[codecOptions.encodingName] = codec;
          break;

        default:
          throw new Error(`Encoding not recognized: '${encoding}' (searched as: '${enc}')`);
      }
    }
    return codec;
  }

  private canonicalizeEncoding(encoding: string): string {
    return encoding.toLowerCase().replace(/:\d{4}$|[^0-9a-z]/g, '');
  }
}

export const iconv: Iconv = new Iconv();
